using System;
using System.Collections.Generic;
using System.Text;
using Microsoft.SharePoint.StsAdmin;
using Microsoft.SharePoint;
using Microsoft.SharePoint.Administration;
using System.Text.RegularExpressions;

namespace CKS.WindowsLiveAuthentication
{
    class WindowsLiveAuthStsAdmCommand : ISPStsadmCommand
    {
        #region Properties
       
        #region Constants
        protected const string MemberResource = @"<add name=""{0}"" type=""CKS.WindowsLiveAuthentication.LiveMembershipProvider, CKS.WindowsLiveAuthentication, Version=1.0.0.0, Culture=neutral, PublicKeyToken=b5adbad25a892e4a"" />";
        protected const string RoleResource = @"<add name=""{0}"" type=""CKS.WindowsLiveAuthentication.LiveRoleProvider, CKS.WindowsLiveAuthentication, Version=1.0.0.0, Culture=neutral, PublicKeyToken=b5adbad25a892e4a"" />";
        protected const string AppIdResource = @"<add key=""WLA_ApplicationId"" value=""{0}"" />";
        protected const string AppKeyResource = @"<add key=""WLA_ApplicationKey"" value=""{0}"" />";
        protected const string AppAlgResource = @"<add key=""WLA_ApplicationAlgorithm"" value=""wsignin1.0"" />";
        protected const string AppModeResource = @"<add key=""WLA_ApplicationMode"" value=""{0}"" />";
        protected const string ProfSiteResource = @"<add key=""WLA_ProfileSiteUrl"" value=""{0}"" />";
        protected const string ProfListResource = @"<add key=""WLA_ProfileList"" value=""{0}"" />";
        protected const string ProfLockedResource = @"<add key=""WLA_LockedUrl"" value=""{0}"" />";
        protected const string FormsResource = @"/_layouts/liveauth-handler.aspx?action=login";
        protected const string className = "CKS.WindowsLiveAuthentication.ProfileListEventHandler";
        protected const string assemblyName = "CKS.WindowsLiveAuthentication, Version=1.0.0.0, Culture=neutral, PublicKeyToken=b5adbad25a892e4a";
        #endregion

        #region Regex
        protected static Regex rulesMatch = new Regex("\"(?<node>[^\"]*)\"", RegexOptions.IgnoreCase | RegexOptions.Compiled);
        #endregion
        
        #endregion

        #region ISPStsadmCommand Members

        public string GetHelpMessage(string command)
        {
            StringBuilder ret = new StringBuilder();
            ret.AppendFormat("In order to obtain some of these values you will need\n");
            ret.AppendFormat("to register your application with Windows Live ID services.\n");
            ret.AppendFormat("You will find the registration form here: https://msm.live.com/app/default.aspx\n");
            ret.AppendFormat("Once you have completed the registration you will have your application id and key\n\n");

            ret.AppendFormat("stsadm -o addwindowsliveauth\n");
            ret.AppendFormat("Required flags:\n");
            ret.AppendFormat("\t-url <URL of the Web Application>\n");
            ret.AppendFormat("\t-appid <application id>\n");
            ret.AppendFormat("\t-appkey <application key>\n");
            ret.AppendFormat("\t-appmode <http/https>\n");
            ret.AppendFormat("\t-profsite <URL of the site that contains the user profile list>\n");
            ret.AppendFormat("\t-proflist <user profile list name>\n");
            ret.AppendFormat("\t-locked <URL to send locked users to>\n");
            ret.AppendFormat("Optional flags:\n");
            ret.AppendFormat("\t-member <membership provider name> [default 'LiveID']\n");
            ret.AppendFormat("\t-role <role provider name> [default 'LiveRoles']\n");

            return ret.ToString();
        }

        public int Run(string command, System.Collections.Specialized.StringDictionary keyValues, out string output)
        {
            output = "";
            StringBuilder ret = new StringBuilder();

            // Check for command line arguments
            bool bUrl = (keyValues.ContainsKey("url") || keyValues.ContainsKey("u"));
            bool bAppId = (keyValues.ContainsKey("appid") || keyValues.ContainsKey("ai"));
            bool bAppKey = (keyValues.ContainsKey("appkey") || keyValues.ContainsKey("ak"));
            bool bAppMode = (keyValues.ContainsKey("appmode") || keyValues.ContainsKey("am"));
            bool bProfSite = (keyValues.ContainsKey("profsite") || keyValues.ContainsKey("ps"));
            bool bProfList = (keyValues.ContainsKey("proflist") || keyValues.ContainsKey("pl"));
            bool bLocked = (keyValues.ContainsKey("locked") || keyValues.ContainsKey("l"));
            bool bMember = (keyValues.ContainsKey("member") || keyValues.ContainsKey("m"));
            bool bRole = (keyValues.ContainsKey("role") || keyValues.ContainsKey("r"));
            bool bBadArgs = false;

            if (!bUrl)
            {
                bBadArgs = true;
                ret.Append("Missing required argument: url\n");
            }
            if (!bAppId)
            {
                bBadArgs = true;
                ret.Append("Missing required argument: appid\n");
            }
            if (!bAppKey)
            {
                bBadArgs = true;
                ret.Append("Missing required argument: appkey\n");
            }
            if (!bAppMode)
            {
                bBadArgs = true;
                ret.Append("Missing required argument: appmode\n");
            }
            if (!bProfSite)
            {
                bBadArgs = true;
                ret.Append("Missing required argument: profsite\n");
            }
            if (!bProfList)
            {
                bBadArgs = true;
                ret.Append("Missing required argument: proflist\n");
            }
            if (!bLocked)
            {
                bBadArgs = true;
                ret.Append("Missing required argument: locked\n");
            }

            if (bBadArgs)
            {
                ret.AppendFormat("\n{0}\n", GetHelpMessage(""));
                output = ret.ToString();
                return 1;
            }

            // Set which command line arg was used "long" form (appid) or "short" form (ai)
            string strUrl = "url";
            string strAppId = "appid";
            string strAppKey = "appkey";
            string strAppMode = "appmode";
            string strProfSite = "profsite";
            string strProfList = "proflist";
            string strLocked = "locked";
            string strMember = "member";
            string strRole = "role";

            if (keyValues.ContainsKey("u"))
            {
                strUrl = "u";
            }
            if (keyValues.ContainsKey("ai"))
            {
                strUrl = "ai";
            }
            if (keyValues.ContainsKey("ak"))
            {
                strUrl = "ak";
            }
            if (keyValues.ContainsKey("am"))
            {
                strUrl = "am";
            }
            if (keyValues.ContainsKey("ps"))
            {
                strUrl = "ps";
            }
            if (keyValues.ContainsKey("pl"))
            {
                strUrl = "pl";
            }
            if (keyValues.ContainsKey("l"))
            {
                strUrl = "l";
            }
            if (keyValues.ContainsKey("m"))
            {
                strMember = "m";
            }
            if (keyValues.ContainsKey("r"))
            {
                strRole = "r";
            }

            // Set the values of the keys
            string url = keyValues[strUrl];
            string appid = keyValues[strAppId];
            string appkey = keyValues[strAppKey];
            string appmode = keyValues[strAppMode];
            string profsite = keyValues[strProfSite];
            string proflist = keyValues[strProfList];
            string locked = keyValues[strLocked];
            string member = "LiveID";
            string role = "LiveRoles";

            // Validate the inputs
            if (bMember && keyValues[strMember].Length > 0)
            {
                member = keyValues[strMember];
            }

            if (bRole && keyValues[strRole].Length > 0)
            {
                role = keyValues[strRole];
            }

            if (appmode != "http" && appmode != "https")
            {
                ret.AppendFormat("Bad format: appmode - http or https only\n\n{0}\n", GetHelpMessage(""));
                output = ret.ToString();
                return 1;
            }

            if (!Uri.IsWellFormedUriString(url, UriKind.Absolute))
            {
                ret.AppendFormat("Bad format: url - Invalid URL format.\n\n{0}\n", GetHelpMessage(""));
                output = ret.ToString();
                return 1;
            }

            if (!Uri.IsWellFormedUriString(profsite, UriKind.Absolute))
            {
                ret.AppendFormat("Bad format: profsite - Invalid URL format.\n\n{0}\n", GetHelpMessage(""));
                output = ret.ToString();
                return 1;
            }

            if (!Uri.IsWellFormedUriString(locked, UriKind.Absolute))
            {
                ret.AppendFormat("Bad format: locked - Invalid URL format.\n\n{0}\n", GetHelpMessage(""));
                output = ret.ToString();
                return 1;
            }

            // Connect to the SharePoint site specified in the -url flag
            using (SPSite site = new SPSite(url))
            using (SPWeb web = site.OpenWeb())
            {
                // Create the User Profile list if it doesn't already exist
                CreateUserProfileList(profsite, proflist);

                // Determine the Web Application that -url is running
                SPWebApplication app = site.WebApplication;

                // Update the web.config to add the Membership provider required XML configuration
                AddAppSettings(app, appid, appkey, appmode, profsite, proflist, locked);
                AddMembershipNodes(app, member, role);
                UpdateAuthorizationForm(app);

                // Merge the changes in to the web.config
                ApplyWebConfigChanges(app);
            }

            output += string.Format("Modifications completed, in order to use the membership provider you must:\n\t1) Run this command again changing the -url parameter\n\t   for every site that you needs to be able to work with these users.\n\t2) Configure the defaultProvider in the membership and roleManager in\n\t    the web.config of the web application you want to use Windows Live\n\t    authentication for.\nWindows Live Authentication has been configured on {0}\n", url);

            return 0;
        }

        /// <summary>
        /// Changes the web.config to point the forms login page to the liveauth-handler.aspx
        /// </summary>
        /// <param name="app">Web Application to change the configuration for</param>
        private void UpdateAuthorizationForm(SPWebApplication app)
        {
            SPWebConfigModification loginurlmode = CreateWebConfigModification(80012, "loginUrl", "configuration/system.web/authentication/forms", FormsResource, SPWebConfigModification.SPWebConfigModificationType.EnsureAttribute);
            app.WebConfigModifications.Add(loginurlmode);
        }

        /// <summary>
        /// Changes the web.config to add the Membership and RoleManager nodes configured to use the Live provider
        /// </summary>
        /// <param name="app">Web Application to change the configuration for</param>
        /// <param name="member">Name of the membership provider</param>
        /// <param name="role">Name of the role provider</param>
        private void AddMembershipNodes(SPWebApplication app, string member, string role)
        {
            SPWebConfigModification membershipmod = CreateWebConfigModification(80008, "membership", "configuration/system.web", "<membership><providers></providers></membership>", SPWebConfigModification.SPWebConfigModificationType.EnsureChildNode);
            SPWebConfigModification membermod = CreateWebConfigModification(80009, "add[@name='LiveID']", "configuration/system.web/membership/providers", string.Format(MemberResource, member), SPWebConfigModification.SPWebConfigModificationType.EnsureChildNode);
            SPWebConfigModification rolemanmod = CreateWebConfigModification(80010, "roleManager", "configuration/system.web", "<roleManager><providers></providers></roleManager>", SPWebConfigModification.SPWebConfigModificationType.EnsureChildNode);
            SPWebConfigModification rolemod = CreateWebConfigModification(80011, "add[@name='LiveRoles']", "configuration/system.web/roleManager/providers", string.Format(RoleResource, role), SPWebConfigModification.SPWebConfigModificationType.EnsureChildNode);

            app.WebConfigModifications.Add(membershipmod);
            app.WebConfigModifications.Add(membermod);
            app.WebConfigModifications.Add(rolemanmod);
            app.WebConfigModifications.Add(rolemod);
        }

        /// <summary>
        /// Changes the web.config to add the appSettings keys required for the Live provider
        /// </summary>
        /// <param name="app">Web Application to chane the configuration for</param>
        /// <param name="appid">Application ID obtained from live.com</param>
        /// <param name="appkey">Secret Key used to register the application on live.com</param>
        /// <param name="appmode">http/https - depending on mode users access the site</param>
        /// <param name="profsite">URL to the site that contains the user profile list</param>
        /// <param name="proflist">Name of the User Profile list</param>
        /// <param name="locked">URL to redirect locked users to - should be anon access</param>
        private void AddAppSettings(SPWebApplication app, string appid, string appkey, string appmode, string profsite, string proflist, string locked)
        {
            SPWebConfigModification appsettmod = CreateWebConfigModification(80000, "appSettings", "configuration", "<appSettings></appSettings>", SPWebConfigModification.SPWebConfigModificationType.EnsureChildNode);
            SPWebConfigModification appidmod = CreateWebConfigModification(80001, "add[@key='WLA_ApplicationId']", "configuration/appSettings", string.Format(AppIdResource, appid), SPWebConfigModification.SPWebConfigModificationType.EnsureChildNode);
            SPWebConfigModification appkeymod = CreateWebConfigModification(80002, "add[@key='WLA_ApplicationKey']", "configuration/appSettings", string.Format(AppKeyResource, appkey), SPWebConfigModification.SPWebConfigModificationType.EnsureChildNode);
            SPWebConfigModification appalgmod = CreateWebConfigModification(80003, "add[@key='WLA_ApplicationAlgorithm']", "configuration/appSettings", AppAlgResource, SPWebConfigModification.SPWebConfigModificationType.EnsureChildNode);
            SPWebConfigModification appmodemod = CreateWebConfigModification(80004, "add[@key='WLA_ApplicationMode']", "configuration/appSettings", string.Format(AppModeResource, appmode), SPWebConfigModification.SPWebConfigModificationType.EnsureChildNode);
            SPWebConfigModification profsitemod = CreateWebConfigModification(80005, "add[@key='WLA_ProfileSiteUrl']", "configuration/appSettings", string.Format(ProfSiteResource, profsite), SPWebConfigModification.SPWebConfigModificationType.EnsureChildNode);
            SPWebConfigModification proflistmod = CreateWebConfigModification(80006, "add[@key='WLA_ProfileList']", "configuration/appSettings", string.Format(ProfListResource, proflist), SPWebConfigModification.SPWebConfigModificationType.EnsureChildNode);
            SPWebConfigModification proflockedmod = CreateWebConfigModification(80007, "add[@key='WLA_LockedUrl']", "configuration/appSettings", string.Format(ProfLockedResource, locked), SPWebConfigModification.SPWebConfigModificationType.EnsureChildNode);

            app.WebConfigModifications.Add(appsettmod);
            app.WebConfigModifications.Add(appidmod);
            app.WebConfigModifications.Add(appkeymod);
            app.WebConfigModifications.Add(appalgmod);
            app.WebConfigModifications.Add(appmodemod);
            app.WebConfigModifications.Add(profsitemod);
            app.WebConfigModifications.Add(proflistmod);
            app.WebConfigModifications.Add(proflockedmod);
        }

        /// <summary>
        /// Applies the changes to the web.config
        /// </summary>
        /// <param name="app">Web Application to commit the web.config changes to</param>
        private void ApplyWebConfigChanges(SPWebApplication app)
        {
            try
            {
                app.WebService.ApplyWebConfigModifications();
            }
            catch (Exception ex)
            {
                // Check the error messages and report accordingly
                MatchCollection matches = rulesMatch.Matches(ex.Message);

                if (matches.Count > 0)
                {
                    // Forms login node only exists on servers using FBA not on the others so squelch the error
                    if (matches[0].Groups["node"].Value == "configuration/system.web/authentication/forms")
                    {
                        app.WebConfigModifications.RemoveAt(app.WebConfigModifications.Count - 1);
                        ApplyWebConfigChanges(app);
                    }
                    else
                    {
                        Console.WriteLine("Error applying web.config modification. Failed to find node \"{0}\".", matches[0].Groups["node"].Value);
                    }
                }
                else
                {
                    Console.WriteLine("ApplyWebConfigChanges() error: {0}", ex.Message);
                }
            }
        }

        /// <summary>
        /// Helper function to create SPWebConfigModification objects
        /// </summary>
        /// <param name="sequence">Sequence of the modification</param>
        /// <param name="name">Name of the modification</param>
        /// <param name="path">Path of the modification</param>
        /// <param name="value">Value of the modification</param>
        /// <param name="wmtype">Type of the modification</param>
        /// <returns>SPWebConfigModification object</returns>
        private static SPWebConfigModification CreateWebConfigModification(uint sequence, string name, string path, string value, SPWebConfigModification.SPWebConfigModificationType wmtype)
        {
            SPWebConfigModification ret = new SPWebConfigModification(name, path);
            ret.Owner = "WindowsLiveAuthProvider";
            ret.Sequence = sequence;
            ret.Type = wmtype;
            ret.Value = value;

            return ret;
        }

        /// <summary>
        /// Will create the User Profile list if it does not already exist.
        /// </summary>
        /// <param name="web">SPWeb to create the list in</param>
        /// <param name="proflist">Name of the List to create</param>
        private void CreateUserProfileList(string url, string proflist)
        {
            using (SPSite site = new SPSite(url))
            {
                using (SPWeb web = site.OpenWeb())
                {
                    try
                    {
                        // Check if the list exists
                        SPList tList = web.Lists[proflist];
                        Console.WriteLine("List {0} already exists, skipping creation\n", proflist);
                    }
                    catch
                    {
                        // List does not exist - create it
                        Console.WriteLine("Creating list {0}\n", proflist);
                        SPListTemplate template = web.ListTemplates["Custom List"];
                        Guid gid = Guid.Empty;
                        Guid gid2 = Guid.Empty;
                        SPList list = null;
                        SPList list2 = null;

                        // Attempt to create the list
                        try
                        {
                            gid = web.Lists.Add(proflist, "Windows Live ID Profile Storage", template);
                            web.Update();
                        }
                        catch (Exception ex)
                        {
                            Console.WriteLine("Could not create list {0} in web {1}.  Error: {2}\n", proflist, web.ServerRelativeUrl, ex.Message);
                        }

                        // Setup the base list configuration
                        try
                        {
                            list = web.Lists[gid];
                            list.OnQuickLaunch = true;
                            list.EnableAttachments = false;
                            list.EnableVersioning = true;
                            list.Fields.Add("DisplayName", SPFieldType.Text, false);
                            list.Fields.Add("Email", SPFieldType.Text, false);
                            list.Fields.Add("AboutMe", SPFieldType.Text, false);
                            list.Fields.Add("Picture", SPFieldType.Text, false);
                            list.Fields.Add("JobTitle", SPFieldType.Text, false);
                            list.Fields.Add("Department", SPFieldType.Text, false);
                            list.Fields.Add("SipAddress", SPFieldType.Text, false);
                            list.Fields.Add("Locked", SPFieldType.Boolean, false);
                            list.Update();
                        }
                        catch (Exception ex)
                        {
                            Console.WriteLine("Could not configure list {0} in web {1}. Error: {2}\n", proflist, web.ServerRelativeUrl, ex.Message);
                        }

                        // Index the title field for performance
                        try
                        {
                            SPField field1 = list.Fields["Title"];
                            field1.Indexed = true;
                            field1.Update();
                        }
                        catch (Exception ex)
                        {
                            Console.WriteLine("Could not configure title field in list {0}. Error: {1}\n", proflist, ex.Message);
                        }

                        // Index the Email field for performance
                        try
                        {
                            SPField field2 = list.Fields["Email"];
                            field2.Indexed = true;
                            field2.Update();
                        }
                        catch (Exception ex)
                        {
                            Console.WriteLine("Could not configure e-mail field in list {0}. Error: {1}\n", proflist, ex.Message);
                        }

                        // Set the default value to false so users are created unlocked
                        try
                        {
                            SPField field3 = list.Fields["Locked"];
                            field3.DefaultValue = "0";
                            field3.Update();
                        }
                        catch (Exception ex)
                        {
                            Console.WriteLine("Could not configure locked field in list {0}. Error: {1}\n", proflist, ex.Message);
                        }

                        // Update the list and web
                        try
                        {
                            list.AllowEveryoneViewItems = false;
                            list.Update();
                            web.Update();
                        }
                        catch (Exception ex)
                        {
                            Console.WriteLine("Could not do final update on list {0} in web {1}. Error: {2}\n", proflist, web.ServerRelativeUrl, ex.Message);
                        }

                                                // Attempt to create the list
                        try
                        {
                            gid2 = web.Lists.Add("LiveProfileSync", "Windows Live ID Profile Sync", template);
                            web.Update();
                        }
                        catch (Exception ex)
                        {
                            Console.WriteLine("Could not create list LiveProfileSync in web {0}.  Error: {1}\n", web.ServerRelativeUrl, ex.Message);
                        }

                        // Setup the base list configuration
                        try
                        {
                            list2 = web.Lists[gid2];
                            list2.OnQuickLaunch = true;
                            list2.EnableAttachments = false;
                            list2.EnableVersioning = true;
                            list2.Fields.Add("Url", SPFieldType.Text, false);
                            list2.EventReceivers.Add(SPEventReceiverType.ItemUpdated, assemblyName, className);
                            list2.Update();
                        }
                        catch (Exception ex)
                        {
                            Console.WriteLine("Could not configure list LiveProfileSync in web {0}. Error: {1}\n", web.ServerRelativeUrl, ex.Message);
                        }

                    }
                }
            }
        }

        #endregion
    }
}
